import { WebSocketServer } from 'ws';

const TICK_MS = 100; // 10Hz
const TILE_COUNT = 25;
const GRID_SIZE = 20;
const ROOM_ID = 'default';

const rooms = new Map();

function createRoom() {
  return {
    id: ROOM_ID,
    status: 'waiting',
    tick: 0,
    startTick: null,
    players: new Map(), // playerId -> {id,name,ws,alive}
    snakes: new Map(), // playerId -> {body:[{x,y}],dir:{dx,dy},alive:true}
    foods: [], // {x,y,type}
    debris: [], // {x,y,amount}
    gas: {
      centerX: Math.floor(TILE_COUNT / 2),
      centerY: Math.floor(TILE_COUNT / 2),
      radius: Math.floor(TILE_COUNT * 0.9),
      shrinkStartTick: 200, // ~20s
      shrinkEndTick: 600 // ~60s
    },
    inputQueue: new Map() // playerId -> latest {dx,dy}
  };
}

function randomInt(n) { return Math.floor(Math.random() * n); }

function spawnFood(room, count = 5) {
  while (room.foods.length < count) {
    const pos = { x: randomInt(TILE_COUNT), y: randomInt(TILE_COUNT) };
    const conflict =
      [...room.snakes.values()].some(s => s.body.some(b => b.x === pos.x && b.y === pos.y)) ||
      room.foods.some(f => f.x === pos.x && f.y === pos.y);
    if (!conflict) {
      room.foods.push({ x: pos.x, y: pos.y, type: pickFoodType() });
    }
  }
}

function pickFoodType() {
  const r = Math.random();
  if (r < 0.6) return 'normal';
  if (r < 0.85) return 'gold';
  if (r < 0.97) return 'special';
  return 'rainbow';
}

function addPlayer(room, ws, name) {
  const id = Math.random().toString(36).slice(2, 10);
  const startX = randomInt(TILE_COUNT);
  const startY = randomInt(TILE_COUNT);
  room.players.set(id, { id, name, ws, alive: true });
  room.snakes.set(id, {
    body: [{ x: startX, y: startY }],
    dir: { dx: 1, dy: 0 },
    alive: true,
    score: 0
  });
  room.inputQueue.set(id, { dx: 1, dy: 0 });
  return id;
}

function removePlayer(room, id) {
  room.players.delete(id);
  room.snakes.delete(id);
  room.inputQueue.delete(id);
}

function stepRoom(room) {
  room.tick++;

  // shrink gas
  if (room.tick >= room.gas.shrinkStartTick && room.tick <= room.gas.shrinkEndTick) {
    const t = (room.tick - room.gas.shrinkStartTick) / (room.gas.shrinkEndTick - room.gas.shrinkStartTick);
    const maxR = Math.floor(TILE_COUNT * 0.9);
    const minR = Math.floor(TILE_COUNT * 0.2);
    room.gas.radius = Math.max(minR, Math.floor(maxR - (maxR - minR) * t));
  }

  // apply inputs and move snakes
  for (const [id, snake] of room.snakes.entries()) {
    if (!snake.alive) continue;
    const input = room.inputQueue.get(id) || snake.dir;
    // prevent 180 turn
    if (!(input.dx === -snake.dir.dx && input.dy === -snake.dir.dy)) {
      snake.dir = input;
    }
    let nx = snake.body[0].x + snake.dir.dx;
    let ny = snake.body[0].y + snake.dir.dy;

    // map bounds (no wrap in battle royale)
    if (nx < 0 || nx >= TILE_COUNT || ny < 0 || ny >= TILE_COUNT) {
      snake.alive = false;
      continue;
    }
    const head = { x: nx, y: ny };
    snake.body.unshift(head);

    // food eat
    const fi = room.foods.findIndex(f => f.x === head.x && f.y === head.y);
    if (fi >= 0) {
      const f = room.foods.splice(fi, 1)[0];
      const growth = f.type === 'normal' ? 1 : f.type === 'gold' ? 2 : f.type === 'special' ? 3 : 5;
      snake.score += f.type === 'normal' ? 10 : f.type === 'gold' ? 50 : f.type === 'special' ? 100 : 200;
      for (let i = 0; i < growth - 1; i++) {
        snake.body.push({ ...snake.body[snake.body.length - 1] });
      }
    } else {
      snake.body.pop();
    }
  }

  // collisions (head with bodies, head-head)
  const aliveIds = [...room.snakes.keys()].filter(id => room.snakes.get(id).alive);
  const occupied = new Map(); // key:"x,y" -> ownerId
  for (const id of aliveIds) {
    const s = room.snakes.get(id);
    for (let i = 0; i < s.body.length; i++) {
      occupied.set(`${s.body[i].x},${s.body[i].y}`, id);
    }
  }
  for (const id of aliveIds) {
    const s = room.snakes.get(id);
    const head = s.body[0];
    // head-body
    for (const [oid, os] of room.snakes.entries()) {
      if (!os.alive) continue;
      for (let i = 0; i < os.body.length; i++) {
        if (oid === id && i === 0) continue; // skip own head
        if (head.x === os.body[i].x && head.y === os.body[i].y) {
          s.alive = false;
        }
      }
    }
  }

  // gas damage: out of circle dies
  for (const id of aliveIds) {
    const s = room.snakes.get(id);
    if (!s.alive) continue;
    const head = s.body[0];
    const dx = head.x - room.gas.centerX;
    const dy = head.y - room.gas.centerY;
    if (Math.sqrt(dx * dx + dy * dy) > room.gas.radius) {
      s.alive = false;
    }
  }

  // convert dead snakes to debris
  for (const [id, s] of room.snakes.entries()) {
    if (!s.alive && s.body.length > 0) {
      const step = Math.max(1, Math.floor(s.body.length / 10));
      for (let i = 0; i < s.body.length; i += step) {
        room.debris.push({ x: s.body[i].x, y: s.body[i].y, amount: 2 });
      }
      s.body = [];
    }
  }

  // debris eat (fast grow)
  for (const [id, s] of room.snakes.entries()) {
    if (!s.alive) continue;
    const head = s.body[0];
    for (let i = room.debris.length - 1; i >= 0; i--) {
      const d = room.debris[i];
      if (d.x === head.x && d.y === head.y) {
        room.debris.splice(i, 1);
        for (let k = 0; k < d.amount; k++) {
          s.body.push({ ...s.body[s.body.length - 1] });
        }
      }
    }
  }

  // respawn foods to a target density
  spawnFood(room, 80);

  // victory
  const aliveAfter = [...room.snakes.values()].filter(s => s.alive);
  if (aliveAfter.length <= 1 && room.status === 'playing') {
    room.status = 'ended';
  }
}

function serializeRoom(room) {
  return {
    type: 'state',
    tick: room.tick,
    status: room.status,
    countdown: room.status === 'starting' ? Math.max(0, Math.ceil((room.startTick - room.tick) / 10)) : 0,
    tileCount: TILE_COUNT,
    gas: room.gas,
    foods: room.foods,
    debris: room.debris,
    snakes: [...room.snakes.entries()].map(([id, s]) => ({ id, body: s.body, alive: s.alive }))
  };
}

function broadcast(room, msg) {
  const text = JSON.stringify(msg);
  for (const p of room.players.values()) {
    if (p.ws.readyState === 1) p.ws.send(text);
  }
}

function ensureRoom() {
  if (!rooms.has(ROOM_ID)) rooms.set(ROOM_ID, createRoom());
  return rooms.get(ROOM_ID);
}

const wss = new WebSocketServer({ port: 8083 });
console.log('WS server listening on :8083');

wss.on('connection', (ws) => {
  const room = ensureRoom();
  const playerId = addPlayer(room, ws, 'guest');
  console.log('player joined', playerId);

  ws.send(JSON.stringify({ type: 'hello', playerId, roomId: ROOM_ID, cfg: { tickMs: TICK_MS, tileCount: TILE_COUNT } }));

  ws.on('message', (data) => {
    try {
      const msg = JSON.parse(data.toString());
      if (msg.type === 'input') {
        // store latest input
        room.inputQueue.set(playerId, msg.dir);
      }
    } catch (e) {
      console.error('bad message', e);
    }
  });

  ws.on('close', () => {
    console.log('player left', playerId);
    removePlayer(room, playerId);
  });
});

// game loop
setInterval(() => {
  const room = ensureRoom();
  if (room.status === 'waiting' && room.players.size >= 1) {
    // 单人即可触发倒计时
    room.status = 'starting';
    room.startTick = room.tick + 30; // 3秒后开始（10Hz）
  }
  if (room.status === 'starting') {
    if (room.tick >= room.startTick) {
      room.status = 'playing';
    }
  }
  if (room.status === 'playing') {
    stepRoom(room);
  }
  broadcast(room, serializeRoom(room));
}, TICK_MS);


